﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Serialization;
using RSSFeedReader.Data.Interop;
using RSSFeedReader.Data.Resources;

namespace RSSFeedReader.Data.Models
{
    /// <summary>
    /// Class used to save and load data.
    /// </summary>
    [Serializable]
    public class ChannelDataSource
    {
        #region Members

        static string DataFileName = "feeds.xml";
        static string ApplicationFolderName = "AEA Systems";
        static string AppDataFolderName = "RSS Feed Reader";
        static string AppDataSaveLocation = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
                Path.Combine(ApplicationFolderName, AppDataFolderName));

        // bool _isSyncWithIE = false;
        int _refreshPeriod = 5;
        ProxySetting _proxySetting;
        List<Channel> _channels;
        static ChannelDataSource instance;

        #endregion

        #region Constructor

        /// <summary>
        /// Returns an instance of the <see cref="RSSFeedReader.Data.Models.ChannelDataSource"/> class.
        /// </summary>
        /// <returns>
        /// An instance of the <see cref="RSSFeedReader.Data.Models.ChannelDataSource"/> class.
        /// </returns>
        static ChannelDataSource GetInstance()
        {
            // If the user does not have a data file load default feeds.
            if (!File.Exists(Path.Combine(AppDataSaveLocation, DataFileName)))
            {
                ChannelDataSource.instance = new ChannelDataSource();
                ChannelDataSource.instance.Channels.AddRange(IEInterop.GetIERSSFeeds());
            }
            else
            {
                ChannelDataSource.instance = ChannelDataSource.Load();

                // In case we failed to load the feeds for some reason.
                if (ChannelDataSource.instance == null)
                {
                    ChannelDataSource.instance.Channels.AddRange(IEInterop.GetIERSSFeeds());
                }
            }

            NetworkChange.NetworkAddressChanged += new NetworkAddressChangedEventHandler(NetworkChangeNetworkAddressChanged);
            ChannelDataSource.instance.IsNetworkAvailable = NetworkInterface.GetIsNetworkAvailable();
            ChannelDataSource.instance.SyncAllChannels();
            return ChannelDataSource.instance;
        }

        static void NetworkChangeNetworkAddressChanged(object sender, EventArgs e)
        {
            if (NetworkInterface.GetIsNetworkAvailable())
            {
                ChannelDataSource.instance.IsNetworkAvailable = true;
            }
            else
            {
                ChannelDataSource.instance.IsNetworkAvailable = false;
            }
        }

        /// <summary>
        /// Initialize a new instance of the <see cref="RSSFeedReader.Data.Models.ChannelDataSource"/> class.
        /// </summary>
        ChannelDataSource()
        {
        }

        #endregion

        #region Events

        #region ChannelLoaded
        /// <summary>
        /// Occurs when a feed has been added.
        /// </summary>
        public event EventHandler<ChannelAddedEventArgs> ChannelAdded;

        /// <summary>
        /// Fires the ChannelAdded event.
        /// </summary>
        /// <param name="sender">
        /// The source of the event.
        /// </param>
        /// <param name="e">
        /// Event arguments describing the event.
        /// </param>
        void OnChannelAdded(object sender, ChannelAddedEventArgs e)
        {
            if (ChannelAdded != null)
                ChannelAdded(sender, e);
        }
        #endregion

        #region ChannelSynchronised
        /// <summary>
        /// Occurs after a feed has been synchronised.
        /// </summary>    
        public event EventHandler<ChannelSynchronisedEventArgs> ChannelSynchronised;

        /// <summary>
        /// Fires the ChannelSynchronised event.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">Event arguments describing the event.</param>
        void OnChannelSynchronised(object sender, ChannelSynchronisedEventArgs e)
        {
            if (ChannelSynchronised != null)
            {
                ChannelSynchronised(sender, e);
            }
        }
        #endregion

        #region ChannelRemoved
        /// <summary>
        /// Occurs when a feed or folder is removed.
        /// </summary>
        public event EventHandler<ChannelRemovedEventArgs> ChannelRemoved;

        /// <summary>
        /// Fires the ChannelRemoved event.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">Event arguments describing the event.</param>
        void OnChannelRemoved(object sender, ChannelRemovedEventArgs e)
        {
            if (ChannelRemoved != null)
                ChannelRemoved(sender, e);
        }
        #endregion

        #region ChannelFolderAdded
        /// <summary>
        /// Occurs when a folder is added.
        /// </summary>
        public event EventHandler<ChannelFolderAddedEventArgs> ChannelFolderAdded;

        /// <summary>
        /// Fires the ChannelFolderAdded event.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">Event arguments describing the event.</param>
        void OnChannelFolderAdded(object sender, ChannelFolderAddedEventArgs e)
        {
            if (ChannelFolderAdded != null)
                ChannelFolderAdded(sender, e);
        }
        #endregion

        #region ChannelMovedToFolder
        /// <summary>
        /// Occurs when a channel/folder is moved.
        /// </summary>
        public event EventHandler<ChannelMovedToFolderEventArgs> ChannelMovedToFolder;

        /// <summary>
        /// Fires the ChannelMovedToFolder event.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">Event arguments describing the event.</param>
        void OnChannelMovedToFolder(object sender, ChannelMovedToFolderEventArgs e)
        {
            if (ChannelMovedToFolder != null)
                ChannelMovedToFolder(sender, e);
        }
        #endregion

        #region ChannelMoved
        /// <summary>
        /// Occurs when a channel/folder is moved.
        /// </summary>
        public event EventHandler<ChannelMovedEventArgs> ChannelMoved;

        /// <summary>
        /// Fires the ChannelMoved event.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">Event arguments describing the event.</param>
        void OnChannelMoved(object sender, ChannelMovedEventArgs e)
        {
            if (ChannelMoved != null)
                ChannelMoved(sender, e);
        }
        #endregion

        #endregion

        #region Public Properties

        /// <summary>
        /// Returns the instance of this class.
        /// </summary>
        public static ChannelDataSource Instance
        {
            get
            {
                if (ChannelDataSource.instance == null)
                {
                    ChannelDataSource.instance = ChannelDataSource.GetInstance();

                    foreach (Channel channel in ChannelDataSource.instance.Channels)
                    {
                        ChannelDataSource.instance.InitializeChildren(channel);
                    }
                }

                return ChannelDataSource.instance;
            }
        }

        /// <summary>
        /// Returns the list of Channels.
        /// </summary>
        public List<Channel> Channels
        {
            get { return _channels ?? (_channels = new List<Channel>()); }
        }

        ///// <summary>
        ///// Gets or sets a value indicating whether or not to sync feeds with Internet Explorer.
        ///// </summary>
        //public bool IsSyncWithIE
        //{
        //    get { return _isSyncWithIE; }
        //    set { _isSyncWithIE = value; }
        //}

        /// <summary>
        /// Gets or sets the refresh period for feeds.
        /// </summary>
        public int RefreshPeriod
        {
            get { return _refreshPeriod; }
            set { _refreshPeriod = value; }
        }

        /// <summary>
        /// Returns true if there is network connectivity.
        /// </summary>
        public bool IsNetworkAvailable { get; set; }

        /// <summary>
        /// Gets the root location of the applications data folder.
        /// </summary>
        public string ApplicationDataLocation
        {
            get { return ChannelDataSource.AppDataSaveLocation; }
        }
        
        /// <summary>
        /// Gets or sets the proxy settings for the application
        /// </summary>
        public ProxySetting ProxySetting
        {
            get { return _proxySetting ?? (_proxySetting = new ProxySetting()); }
            set { _proxySetting = value; }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Add a new <see cref="RSSFeedReader.Data.Models.Channel"/> to the data.
        /// </summary>
        /// <param name="url">
        /// The url of the <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </param>
        public void AddChannel(string url)
        {
            LoadChannelAsync(url);
        }

        /// <summary>
        /// Add a new <see cref="RSSFeedReader.Data.Models.Channel"/> to the data.
        /// </summary>
        /// <param name="url">
        /// The url of the <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </param>
        /// <param name="parent">The parent <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </param>
        public void AddChannel(string url, Channel parent)
        {
            LoadChannelAsync(url, parent);
        }

        /// <summary>
        /// Save the current data to user app data file.
        /// </summary>
        public void Save()
        {
            if (_channels == null)
                return;

            Directory.CreateDirectory(AppDataSaveLocation);

            XmlSerializer xmlSer = new XmlSerializer(typeof(ChannelDataSource));
            using (Stream stream = new FileStream(Path.Combine(AppDataSaveLocation, DataFileName),
                FileMode.Create, FileAccess.Write, FileShare.None))
            {
                xmlSer.Serialize(stream, this);
            }
        }

        /// <summary>
        /// Checks if there is a <see cref="RSSFeedReader.Data.Models.Channel"/> object in 
        /// the <see cref="RSSFeedReader.Data.Models.ChannelDataSource"/> instance with the specified url.
        /// </summary>
        /// <param name="url">
        /// The url of the <see cref="RSSFeedReader.Data.Models.Channel"/> to look for.
        /// </param>
        /// <returns>
        /// True if a <see cref="RSSFeedReader.Data.Models.Channel"/> object is found, false otherwise.
        /// </returns>
        public bool ContainsChannel(string url)
        {
            return FindChannelByUrl(url) != null;
        }

        /// <summary>
        /// Checks if there is a <see cref="RSSFeedReader.Data.Models.Channel"/> object of ChannelFeedType Folder in 
        /// the <see cref="RSSFeedReader.Data.Models.ChannelDataSource"/> instance with the specified title.
        /// </summary>
        /// <param name="title">
        /// The title of the <see cref="RSSFeedReader.Data.Models.Channel"/> to look for.
        /// </param>
        /// <param name="parentChannel">The parent <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </param>
        /// <returns>
        /// True if a <see cref="RSSFeedReader.Data.Models.Channel"/> object is found, false otherwise.
        /// </returns>
        public bool ContainsFolder(string title, Channel parentChannel)
        {
            return FindFolderByTitle(title, parentChannel) != null;
        }

        /// <summary>
        /// Check if the specified url string is a valid Uri.
        /// </summary>
        /// <param name="url">
        /// The string url to validate.
        /// </param>
        /// <returns>
        /// True if the url is valid, false otherwise.
        /// </returns>
        public bool IsValidUrl(string url)
        {
            string feedUrl = url;

            Uri newUri = null;

            if (Uri.TryCreate(feedUrl, UriKind.Absolute, out newUri))
            {
                // If the URL references a local file, but the file does not
                // exist, then the URL is considered to be invalid.
                bool isLocalFile = newUri.IsLoopback;
                if (isLocalFile && !System.IO.File.Exists(newUri.LocalPath))
                    newUri = null;
            }

            return newUri != null;
        }

        /// <summary>
        /// Removes the <see cref="RSSFeedReader.Data.Models.Channel"/> with the specified url.
        /// </summary>
        /// <param name="url">
        /// The url of the <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </param>
        public void RemoveChannel(string url)
        {
            Channel channelToRemove = FindChannelByUrl(url);
            _channels.Remove(channelToRemove);

            // Raise the event
            if (ChannelRemoved != null)
                ChannelRemoved(this, new ChannelRemovedEventArgs(channelToRemove));
        }

        /// <summary>
        /// Removes the <see cref="RSSFeedReader.Data.Models.Channel"/> with the specified ID.
        /// </summary>
        /// <param name="id">The ID of the <see cref="RSSFeedReader.Data.Models.Channel"/>.</param>
        public void RemoveChannel(Guid id)
        {
            Channel channelToRemove = FindChannelById(id);

            // Try and find the parent
            if (channelToRemove.ParentID != Guid.Empty)
            {
                Channel parent = FindChannelById(channelToRemove.ParentID);
                parent.Children.Remove(channelToRemove);
            }
            else
            {
                _channels.Remove(channelToRemove);
            }

            OnChannelRemoved(this, new ChannelRemovedEventArgs(channelToRemove));
        }

        /// <summary>
        /// Removes all the Channels.
        /// </summary>
        public void RemoveAllChannels()
        {
            _channels.Clear();
        }

        /// <summary>
        /// Synch a Channel with the specified url.
        /// </summary>
        /// <param name="id">
        /// The ID of the Channel.
        /// </param>
        public void SyncChannel(Guid id)
        {
            SyncChannelAsynch(FindChannelById(id));
        }

        /// <summary>
        /// Synch all the Channels.
        /// </summary>
        public void SyncAllChannels()
        {
            _channels.ForEach(x => SyncChannelAsynch(x));

            foreach (Channel channel in _channels)
            {
                SyncChannel(channel);
            }
        }

        /// <summary>
        /// Adds a new folder to the channels.
        /// </summary>
        /// <param name="title">Title of the folder.</param>
        public Channel AddNewFolder(string title)
        {
            Channel newFolder = new Channel(Guid.NewGuid(), title, string.Empty,
                string.Empty, string.Empty, string.Empty);
            newFolder.ChannelType = ChannelType.Folder;
            _channels.Add(newFolder);
            OnChannelFolderAdded(this, new ChannelFolderAddedEventArgs(newFolder));

            return newFolder;
        }

        /// <summary>
        /// Add a new folder to the channels under another folder.
        /// </summary>
        /// <param name="parentID">ID of the parent <see cref="RSSFeedReader.Data.Models.Channel"/>.</param>
        /// <param name="title">Title of the folder.</param>
        public void AddNewFolder(Guid parentID, string title)
        {
            Channel parent = FindChannelById(parentID);

            if (parent == null)
                return;

            Channel newFolder = new Channel(Guid.NewGuid(), parent, title, string.Empty,
                string.Empty, string.Empty, string.Empty);
            newFolder.ChannelType = ChannelType.Folder;
            parent.Children.Add(newFolder);
            OnChannelFolderAdded(this, new ChannelFolderAddedEventArgs(parent, newFolder));
        }

        /// <summary>
        /// Checks if the a channel with the specied ID
        /// is of the Feed ChannelType.
        /// </summary>
        /// <param name="id">The ID of the <see cref="Channel"/> to check its ChannelType.</param>
        /// <returns>True if the <see cref="Channel"/> is of the Feed ChannelType.</returns>
        public bool IsChannelTypeFeed(Guid id)
        {
            return FindChannelById(id).ChannelType == ChannelType.Feed;
        }

        /// <summary>
        /// Checks if the a channel with the specied ID
        /// is of the Folder ChannelType.
        /// </summary>
        /// <param name="id">The ID of the <see cref="Channel"/> to check its ChannelType.</param>
        /// <returns>True if the <see cref="Channel"/> is of the Folder ChannelType.</returns>
        public bool IsChannelTypeFolder(Guid id)
        {
            try
            {
                return FindChannelById(id).ChannelType == ChannelType.Folder;
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        /// Moves a channel or folder to a specified folder.
        /// </summary>
        /// <param name="channelID">The ID of the channel to move.</param>
        /// <param name="folderID">The ID of the destination folder.</param>
        public void MoveToFolder(Guid channelID, Guid folderID)
        {
            if (channelID == null)
                throw new ArgumentNullException("channelID");

            if (folderID == null)
                throw new ArgumentNullException("folderID");

            Channel channelToMove = FindChannelById(channelID);
            Channel folder = FindChannelById(folderID);

            Debug.Assert(channelToMove != null);

            if (channelToMove.ParentID != Guid.Empty && folder != null)
            {
                // Check folder does not all ready contain the channel
                if (folder.Children.Contains(channelToMove))
                    return;

                // Moving from folder to folder
                Channel parent = FindChannelById(channelToMove.ParentID);
                parent.Children.Remove(channelToMove);
                folder.Children.Add(channelToMove);
                channelToMove.ParentID = folder.ChannelID;
                OnChannelMovedToFolder(this,
                    new ChannelMovedToFolderEventArgs(channelToMove, folder, parent));
            }
            else if (folder != null)
            {
                // Check folder does not all ready contain the channel
                if (folder.Children.Contains(channelToMove))
                    return;

                // Coming from root to a folder
                ChannelDataSource.instance.Channels.Remove(channelToMove);
                folder.Children.Add(channelToMove);
                channelToMove.ParentID = folder.ChannelID;
                OnChannelMovedToFolder(this,
                    new ChannelMovedToFolderEventArgs(channelToMove, folder));
            }
            else
            {
                // Must be moving back to the root

                // Check root doesn't all ready contain the channel
                if (ChannelDataSource.instance.Channels.Contains(channelToMove))
                    return;

                Channel parent = FindChannelById(channelToMove.ParentID);
                parent.Children.Remove(channelToMove);
                channelToMove.ParentID = Guid.Empty;
                ChannelDataSource.instance.Channels.Add(channelToMove);
                ChannelMovedToFolder(this,
                    new ChannelMovedToFolderEventArgs(channelToMove));
            }
        }

        /// <summary>
        /// Moves a channel to another position before the specified toID.
        /// </summary>
        /// <param name="fromID">The ID of the channel to move.</param>
        /// <param name="toID">The ID of the channel to move before.</param>
        public void MoveChannel(Guid fromID, Guid toID)
        {
            if (fromID == null)
                throw new ArgumentNullException("fromID");

            if (toID == null)
                throw new ArgumentNullException("toID");

            Channel channelToMove;
            Channel channelToMoveParent;
            Channel channelToMoveTo;
            Channel channelToMoveToParent;
            int channelToMoveToIndex = -1;
            List<Channel> channelsOriginal = _channels.ToList(); ;

            try
            {
                channelToMove = FindChannelById(fromID);
                Debug.Assert(channelToMove != null);
                channelToMoveParent = FindChannelById(channelToMove.ParentID);

                if (toID == Guid.Empty)
                {
                    // Must be moving back to the root
                    channelToMoveParent.Children.Remove(channelToMove);
                    channelToMove.ParentID = Guid.Empty;
                    ChannelDataSource.instance.Channels.Add(channelToMove);
                    ChannelMoved(this, new ChannelMovedEventArgs(channelToMove));
                    return;
                }

                channelToMoveTo = FindChannelById(toID);
                Debug.Assert(channelToMoveTo != null);
                channelToMoveToParent = FindChannelById(channelToMoveTo.ParentID);

                if (channelToMove == channelToMoveTo)
                {
                    return;
                }
                else if (channelToMoveTo.ChannelType == ChannelType.Folder)
                {
                    // Moving to a folder
                    MoveToFolder(fromID, toID);
                }
                else if (channelToMoveParent != null && channelToMoveToParent != null)
                {
                    channelToMoveToIndex = channelToMoveToParent.Children.IndexOf(channelToMoveTo);

                    if (channelToMoveParent == channelToMoveToParent)
                    {
                        // Moving from within the same folder
                        if (channelToMoveTo == channelToMoveToParent.Children.Last())
                        {
                            channelToMoveParent.Children.Remove(channelToMove);
                            channelToMoveToParent.Children.Add(channelToMove);
                        }
                        else if (channelToMoveTo == channelToMoveToParent.Children.First())
                        {
                            channelToMoveParent.Children.Remove(channelToMove);
                            channelToMoveToParent.Children.Insert(0, channelToMove);
                        }
                        else
                        {
                            channelToMoveParent.Children.Remove(channelToMove);
                            channelToMoveToParent.Children.Insert(channelToMoveToIndex, channelToMove);
                        }
                        channelToMove.ParentID = channelToMoveToParent.ChannelID;
                    }
                    else
                    {
                        // Moving from one folder to another
                        channelToMoveParent.Children.Remove(channelToMove);

                        if (channelToMoveTo == channelToMoveToParent.Children.Last())
                        {
                            channelToMoveToParent.Children.Add(channelToMove);
                        }
                        else if (channelToMoveTo == channelToMoveToParent.Children.First())
                        {
                            channelToMoveToParent.Children.Insert(0, channelToMove);
                        }
                        else
                        {
                            channelToMoveToParent.Children.Insert(channelToMoveToIndex, channelToMove);
                        }
                        channelToMove.ParentID = channelToMoveToParent.ChannelID;
                    }

                    OnChannelMoved(this, new ChannelMovedEventArgs(channelToMove, channelToMoveTo));
                }
                else if (channelToMoveParent == null && channelToMoveToParent != null)
                {
                    // Coming from root to a folder
                    channelToMoveToIndex = channelToMoveToParent.Children.IndexOf(channelToMoveTo);
                    ChannelDataSource.instance.Channels.Remove(channelToMove);

                    if (channelToMoveTo == channelToMoveToParent.Children.Last())
                    {
                        channelToMoveToParent.Children.Add(channelToMove);
                    }
                    else if (channelToMoveTo == channelToMoveToParent.Children.First())
                    {
                        channelToMoveToParent.Children.Insert(0, channelToMove);
                    }
                    else
                    {
                        channelToMoveToParent.Children.Insert(channelToMoveToIndex + 1, channelToMove);
                    }

                    channelToMove.ParentID = channelToMoveToParent.ChannelID;
                    OnChannelMoved(this, new ChannelMovedEventArgs(channelToMove, channelToMoveTo));
                }
                else if (channelToMoveParent != null && channelToMoveToParent == null)
                {
                    // Coming from folder to root at a specified feed
                    channelToMoveToIndex = _channels.IndexOf(channelToMoveTo);
                    channelToMoveParent.Children.Remove(channelToMove);

                    if (channelToMoveTo == _channels.Last())
                    {
                        _channels.Add(channelToMove);
                    }
                    else if (channelToMoveTo == _channels.First())
                    {
                        _channels.Insert(0, channelToMove);
                    }
                    else
                    {
                        _channels.Insert(channelToMoveToIndex + 1, channelToMove);
                    }

                    channelToMove.ParentID = Guid.Empty;
                    OnChannelMoved(this, new ChannelMovedEventArgs(channelToMove, channelToMoveTo));
                }
                else if (channelToMoveParent == null && channelToMoveToParent == null)
                {
                    channelToMoveToIndex = _channels.IndexOf(channelToMoveTo);

                    if (channelToMoveTo == _channels.Last())
                    {
                        _channels.Remove(channelToMove);
                        _channels.Add(channelToMove);
                    }
                    else if (channelToMoveTo == _channels.First())
                    {
                        _channels.Remove(channelToMove);
                        _channels.Insert(0, channelToMove);
                    }
                    else
                    {
                        _channels.Remove(channelToMove);
                        _channels.Insert(channelToMoveToIndex, channelToMove);
                    }

                    OnChannelMoved(this, new ChannelMovedEventArgs(channelToMove, channelToMoveTo));
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
                // Return to original state
                _channels = channelsOriginal;
            }
        }

        /// <summary>
        /// Finds a channel with the specified ID.
        /// </summary>
        /// <param name="id">The ID of the Channel to find.</param>
        /// <returns>The Channel if found, null otherwise</returns>
        public Channel FindChannelById(Guid id)
        {
            Channel foundChannel = null;

            foreach (Channel channel in Channels)
            {
                foundChannel = FindChannelById(channel, id).FirstOrDefault();

                if (foundChannel != null)
                    return foundChannel;
            }

            Debug.WriteLine(string.Format("No Channel with '{0}' id could be found, why?", id));
            return null;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Initialize any children in the feeds.
        /// </summary>
        /// <param name="channel">The Channel to search for children within.</param>
        void InitializeChildren(Channel channel)
        {
            if (channel.Children.Count() > 0)
            {
                foreach (Channel child in channel.Children)
                {
                    child.ParentID = channel.ChannelID;
                    ChannelDataSource.instance.InitializeChildren(child);
                }
            }
        }

        /// <summary>
        /// Loads the user settings from the data file.
        /// </summary>
        /// <returns>
        /// An instance of the <see cref="RSSFeedReader.Data.Models.ChannelDataSource"/> class.
        /// </returns>
        static ChannelDataSource Load()
        {
            try
            {
                XmlSerializer xmlSer = new XmlSerializer(typeof(ChannelDataSource));
                using (Stream stream = new FileStream(Path.Combine(AppDataSaveLocation, DataFileName),
                    FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    ChannelDataSource channelDataSource = (ChannelDataSource)xmlSer.Deserialize(stream);
                    stream.Close();
                    return channelDataSource;
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(string.Format("Error deserializing file!!\n{0}", ex.Message));
            }

            return null;
        }

        /// <summary>
        /// Searches the specified channels children
        /// and syncs them.
        /// </summary>
        /// <param name="channel">The Channel to search its children.</param>
        void SyncChannel(Channel channel)
        {
            if (channel.Children.Count > 0)
            {
                foreach (Channel channelChild in channel.Children)
                    SyncChannel(channelChild);
            }
            else
            {
                if (IsChannelTypeFolder(channel.ChannelID))
                    return;

                SyncChannelAsynch(channel);
            }
        }

        /// <summary>
        /// Load a feed with the specified url.
        /// </summary>
        /// <param name="url">
        /// The url of the feed.
        /// </param>
        void LoadChannelAsync(string url)
        {
            if (!IsNetworkAvailable)
                return;

            BackgroundWorker loadChannelWorker = new BackgroundWorker();
            loadChannelWorker.DoWork += new DoWorkEventHandler(LoadChannelWorkerDoWork);
            loadChannelWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(LoadChannelWorkerRunWorkerCompleted);
            Object[] args = new Object[] { url };
            loadChannelWorker.RunWorkerAsync(args);
        }

        /// <summary>
        /// Load a feed with the specified url.
        /// </summary>
        /// <param name="url">
        /// The url of the feed.
        /// </param>
        /// <param name="parent">The parent <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </param>
        void LoadChannelAsync(string url, Channel parent)
        {
            if (!IsNetworkAvailable)
                return;

            BackgroundWorker loadChannelWorker = new BackgroundWorker();
            loadChannelWorker.DoWork += new DoWorkEventHandler(LoadChannelWorkerDoWork);
            loadChannelWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(LoadChannelWorkerRunWorkerCompleted);
            object[] args = new object[] { url, parent };
            loadChannelWorker.RunWorkerAsync(args);
        }

        /// <summary>
        /// Event handler for downloading the feed asynchronously.
        /// </summary>
        /// <param name="sender">
        /// The source of the event.
        /// </param>
        /// <param name="e">
        /// Event arguments describing the event.
        /// </param>
        void LoadChannelWorkerDoWork(object sender, DoWorkEventArgs e)
        {
            object[] args= e.Argument as object[];
            String url = args[0] as String;
            Channel parent = null;
            Channel newChannel = null;
            object[] result = null;

            newChannel = LoadChannel(url);

            if (args.Count() > 1)
            {
                parent = args[1] as Channel;
                result = new object[] { newChannel, parent };
            }
            else
            {
                result = new object[] { newChannel };
            }

            e.Result = result;
        }

        /// <summary>
        /// Loads the specified url feed and returns the  <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </summary>
        /// <param name="url">
        /// The url of the <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </param>
        /// <returns>
        /// An instance of the <see cref="RSSFeedReader.Data.Models.Channel"/> class.
        /// </returns>
        /// <exception cref="RSSFeedReader.Data.Models.FeedLoadException">
        /// An exception occured while trying to parse the xml from the specified url.
        /// </exception>
        Channel LoadChannel(string url)
        {
            XElement rssXml = null;
            IEnumerable<Channel> channels = null;
            IEnumerable<Post> posts = null;
            Channel channel;

            try
            {
                rssXml = XElement.Load(GetXml(url));
                rssXml = StripNameSpaces(rssXml);

                channels = rssXml.Descendants("channel").Select(
                    x => new Channel(
                        Guid.NewGuid(),
                        x.Element("title").Value.Trim(),
                        x.Element("description").Value.Trim(),
                        x.Element("link").Value,
                        GetChannelImage(x),
                        url));

                channel = channels.FirstOrDefault();

                posts = rssXml.Descendants("item").Select(
                    item => new Post(
                        item.Element("title").Value.Trim(),
                        StripHtml(item.Element("description").Value),
                        item.Element("link").Value,
                        GetDate(item)
                        )
                        {
                            IsRead = FindReadPost(channel, item.Element("link").Value)
                        });

                if (channel == null)
                {
                    throw new Exception("Error whilst adding channel");
                }

                channel.Posts = posts.ToList();

            }
            catch (XmlException ex)
            {
                throw new Models.FeedLoadException(string.Format(Strings.ChannelDataSource_XmlException, url),
                    url, ex);
            }
            catch (System.Net.WebException ex)
            {
                throw new Models.FeedLoadException(string.Format(Strings.ChannelDataSource_UnhandledException, url),
                    url, ex);
            }
            catch (Exception ex)
            {
                throw new Models.FeedLoadException(string.Format(Strings.ChannelDataSource_UnhandledException, url),
                    url, ex);
            }

            return channel;
        }

        /// <summary>
        /// Gets the url of the image associated with the feed.
        /// </summary>
        /// <param name="item">
        /// The XElement item to get the image url from.
        /// </param>
        /// <returns>
        /// The url of the image.
        /// </returns>
        string GetChannelImage(XElement item)
        {
            if (item == null)
                throw new ArgumentNullException("item");

            XElement image = item.Element("image");
            if (image != null)
            {
                XElement imageUrl = image.Element("url");
                if (imageUrl != null)
                    return imageUrl.Value;
            }

            return null;
        }

        /// <summary>
        /// Event handler for when the feed has been downloaded.
        /// </summary>
        /// <param name="sender">
        /// The source of the event.
        /// </param>
        /// <param name="e">
        /// Event arguments describing the event.
        /// </param>
        void LoadChannelWorkerRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                OnChannelAdded(sender, new ChannelAddedEventArgs(e.Error as FeedLoadException, false, null));
                return;
            }

            object [] result = e.Result as object[];
            Channel channel = result[0] as Channel;
            Channel parent = null;

            if (result.Count() > 1)
            {
                _channels.Add(channel);
                OnChannelAdded(this, new ChannelAddedEventArgs(channel, null));
                parent = result[1] as Channel;
                ChannelDataSource.Instance.MoveChannel(channel.ChannelID, parent.ChannelID);
            }
            else
            {
                _channels.Add(channel);
                OnChannelAdded(this, new ChannelAddedEventArgs(channel, null));
            }
        }

        /// <summary>
        /// Synch the posts for the specified <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </summary>
        /// <param name="channelToSynch">
        /// The <see cref="RSSFeedReader.Data.Models.Channel"/> to synch the posts for.
        /// </param>
        void SyncChannelAsynch(Channel channelToSynch)
        {
            if (channelToSynch.ChannelType == ChannelType.Folder ||
                !IsNetworkAvailable)
                return;

            BackgroundWorker syncChannelsWorker = new BackgroundWorker();
            syncChannelsWorker.DoWork += new DoWorkEventHandler(SyncChannelsWorkerDoWork);
            syncChannelsWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(SyncChannelsWorkerRunWorkerCompleted);
            syncChannelsWorker.RunWorkerAsync(channelToSynch);
        }

        /// <summary>
        /// Event handler for synching the feed asynchronously.
        /// </summary>
        /// <param name="sender">
        /// The source of the event.
        /// </param>
        /// <param name="e">
        /// Event arguments describing the event.
        /// </param>
        void SyncChannelsWorkerDoWork(object sender, DoWorkEventArgs e)
        {
            Channel channel = e.Argument as Channel;
            e.Result = LoadPosts(channel);
        }

        /// <summary>
        /// Load the posts asynchronously for the specified <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </summary>
        /// <param name="channel">
        /// The <see cref="RSSFeedReader.Data.Models.Channel"/> instance to load the posts from.
        /// </param>
        /// <returns>
        /// An instance of the <see cref="RSSFeedReader.Data.Models.Channel"/> class.
        /// </returns>
        /// <exception cref="RSSFeedReader.Data.Models.FeedLoadException">
        /// An exception occured while trying to parse the xml associated with the specified
        /// <see cref="RSSFeedReader.Data.Models.Channel"/> instance.
        /// </exception>
        [DebuggerStepThrough()]
        Channel LoadPosts(Channel channel)
        {
            IEnumerable<Post> posts = null;
            XElement rssFeed = null;

            try
            {
                rssFeed = XElement.Load(GetXml(channel.Url));
                rssFeed = StripNameSpaces(rssFeed);
                posts = rssFeed.Descendants("item").Select(
                    item => new Post(
                            item.Element("title").Value.Trim(),
                            StripHtml(item.Element("description").Value),
                            item.Element("link").Value,
                            GetDate(item)
                        )
                    {
                        IsRead = FindReadPost(channel, item.Element("link").Value)
                    });

                channel.Posts = posts.ToList();
            }
            catch (XmlException ex)
            {
                throw new FeedSynchroniseException(string.Format(Strings.ChannelDataSource_XmlException, channel.Url),
                    channel.Url, channel.ChannelID, ex);
            }
            catch (Exception ex)
            {
                throw new FeedSynchroniseException(string.Format(Strings.ChannelDataSource_UnhandledException, channel.Url),
                    channel.Url, channel.ChannelID, ex);
            }

            return channel;
        }

        /// <summary>
        /// Gets a value indicating whether or not this post has been read.
        /// </summary>
        /// <param name="channel">The <see cref="RSSFeedReader.Data.Models.Channel"/> the 
        /// <see cref="RSSFeedReader.Data.Models.Post"/> belongs to.</param>
        /// <param name="postLink">The link to the Post.</param>
        /// <returns>True if the post has been read, false otherwise.</returns>
        bool FindReadPost(Channel channel, string postLink)
        {
            foreach (Post post in channel.Posts)
            {
                if (post.Link == postLink)
                {
                    return post.IsRead;
                }
            }

            return false;
        }

        /// <summary>
        /// Event handler for when the feed has been synched.
        /// </summary>
        /// <param name="sender">
        /// The source of the event.
        /// </param>
        /// <param name="e">
        /// Event arguments describing the event.
        /// </param>
        void SyncChannelsWorkerRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                OnChannelSynchronised(sender, new ChannelSynchronisedEventArgs(e.Error as FeedSynchroniseException, false, null));
                return;
            }

            if (_channels.Count == 0)
                return;

            Channel channel = e.Result as Channel;
            Channel channelToSynch = FindChannelById(channel.ChannelID);

            if (channelToSynch == null)
                return;

            channelToSynch.Posts = channel.Posts;

            // Raise the ChannelSynched Event.
            OnChannelSynchronised(sender, new ChannelSynchronisedEventArgs(channel, null));
        }

        /// <summary>
        /// Gets the published date of the feed.
        /// </summary>
        /// <param name="item">
        /// The XElement item to get the published date from.
        /// </param>
        /// <returns>
        /// The published date string.
        /// </returns>
        string GetDate(XElement item)
        {
            if (item == null)
                throw new ArgumentNullException("item");

            XElement pubDate = item.Element("pubDate");

            if (pubDate == null)
            {
                pubDate = item.Element("date");
            }

            return pubDate == null ? string.Empty : pubDate.Value;
        }

        /// <summary>
        /// Removes name space prefixes from the elements.
        /// </summary>
        /// <param name="xElement">
        /// The XElement to remove the namespace prefixes from.
        /// </param>
        /// <returns>
        /// An XElement without namespace prefixes.
        /// </returns>
        XElement StripNameSpaces(XElement xElement)
        {
            string xml = xElement.ToString();
            // Remove name space declarations and characters
            xml = xml.Replace("dc:", "");
            xml = xml.Replace("slash:", "");
            xml = xml.Replace("â€“", "");
            xml = xml.Replace("â€¦", "");

            XElement element = XElement.Parse(xml);
            return element;
        }

        /// <summary>
        /// Strips the HTML parts of the description.
        /// </summary>
        string StripHtml(string html)
        {
            try
            {
                if (String.IsNullOrEmpty(html))
                    return String.Empty;

                // Get rid of any HTML-encoded characters, like converting &lt; to '<'
                string decodedHtml = System.Web.HttpUtility.HtmlDecode(html);

                // Remove all <open> and </close> tags.
                StringBuilder text = new StringBuilder(Regex.Replace(decodedHtml, "<[^>]*>", String.Empty));

                const string TWO_SPACES = "  ";
                const string ONE_SPACE = " ";

                text.Replace(TWO_SPACES, ONE_SPACE);
                text.Replace("\t", ONE_SPACE);
                text.Replace("\n", ONE_SPACE);

                // Limit the length of the description
                if (text.ToString().Length > 500)
                    text = new StringBuilder(text.ToString().Substring(0, 500) + "...");

                return text.ToString();
            }
            catch (Exception e)
            {
                Debug.WriteLine(string.Format("Error!! Message: {0}", e.Message));
                return html;
            }
        }

        /// <summary>
        /// Finds a <see cref="RSSFeedReader.Data.Models.Channel"/> in the local collection based on the url.
        /// </summary>
        /// <param name="url">
        /// The url to look for in the Lists of <see cref="RSSFeedReader.Data.Models.Channel"/>s.
        /// </param>
        /// <returns>
        /// A <see cref="RSSFeedReader.Data.Models.Channel"/> instance if found, null otherwise.
        /// </returns>
        Channel FindChannelByUrl(string url)
        {
            Channel foundChannel = null;

            foreach (Channel channel in Channels)
            {
                foundChannel = FindChannelByUrl(channel, url).FirstOrDefault();

                if (foundChannel != null)
                    return foundChannel;
            }

            Debug.WriteLine(string.Format("No Channel with '{0}' url could be found, why?", url));
            return null;
        }

        /// <summary>
        /// Finds a <see cref="RSSFeedReader.Data.Models.Channel"/> instance,
        /// whose url is equal to the url passed in.
        /// </summary>
        /// <param name="channel">
        /// The <see cref="RSSFeedReader.Data.Models.Channel"/> to test against
        /// the condition.
        /// </param>
        /// <param name="url">
        /// The url to compare against.
        /// </param>
        /// <returns>
        /// A IEnumerable collection of type <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </returns>
        IEnumerable<Channel> FindChannelByUrl(Channel channel, string url)
        {
            if (channel.Url == url)
                yield return channel;

            foreach (Channel child in channel.Children)
                foreach (Channel isSameUrl in FindChannelByUrl(child, url))
                    yield return isSameUrl;
        }

        /// <summary>
        /// Finds a <see cref="RSSFeedReader.Data.Models.Channel"/> instance,
        /// whose id is equal to the id passed in.
        /// </summary>
        /// <param name="channel">
        /// The <see cref="RSSFeedReader.Data.Models.Channel"/> to test against
        /// the condition.</param>
        /// <param name="id">The id to compare against.</param>
        /// <returns> 
        /// A IEnumerable collection of type <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </returns>
        IEnumerable<Channel> FindChannelById(Channel channel, Guid id)
        {
            if (channel.ChannelID == id)
                yield return channel;

            foreach (Channel child in channel.Children)
                foreach (Channel foundChannel in FindChannelById(child, id))
                    yield return foundChannel;
        }

        /// <summary>
        /// Finds a <see cref="RSSFeedReader.Data.Models.Channel"/> in the local collection based on the title.
        /// </summary>
        /// <param name="title">
        /// The title to look for in the Lists of <see cref="RSSFeedReader.Data.Models.Channel"/>s.
        /// </param>
        /// <param name="parentChannel">The parent <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </param>
        /// <returns>
        /// A <see cref="RSSFeedReader.Data.Models.Channel"/> instance if found, null otherwise.
        /// </returns>
        Channel FindFolderByTitle(string title, Channel parentChannel)
        {
            Channel foundFolder = null;

            foreach (Channel channel in Channels)
            {
                foundFolder = FindFolderByTitle(channel, title).FirstOrDefault();

                if (parentChannel != null)
                {
                    if (foundFolder != null)
                        return foundFolder;
                }
                else
                {
                    if (foundFolder != null && foundFolder.ParentID == parentChannel.ChannelID)
                        return foundFolder;
                }
            }

            Debug.WriteLine(string.Format("No Folder with '{0}' title could be found", title));
            return null;
        }

        /// <summary>
        /// Finds a <see cref="RSSFeedReader.Data.Models.Channel"/> instance,
        /// whose title is equal to the title passed in.
        /// </summary>
        /// <param name="channel">
        /// The <see cref="RSSFeedReader.Data.Models.Channel"/> to test against
        /// the condition.
        /// </param>
        /// <param name="title">
        /// The title to compare against.
        /// </param>
        /// <returns>
        /// A IEnumerable collection of type <see cref="RSSFeedReader.Data.Models.Channel"/>.
        /// </returns>
        IEnumerable<Channel> FindFolderByTitle(Channel channel, string title)
        {
            if (channel.Title == title && channel.ChannelType == ChannelType.Folder)
                yield return channel;

            foreach (Channel child in channel.Children)
                foreach (Channel isSameTitle in FindChannelByUrl(child, title))
                    yield return isSameTitle;
        }

        XmlTextReader GetXml(string url)
        {
            try
            {
                WebRequest webRequest = WebRequest.Create(url);

                if (_proxySetting != null)
                { 
                    webRequest.Proxy = new WebProxy(_proxySetting.Address, _proxySetting.Port);
                    webRequest.Proxy.Credentials = new NetworkCredential(_proxySetting.UserName, _proxySetting.Password, _proxySetting.Domain);
                }

                return new XmlTextReader(webRequest.GetResponse().GetResponseStream());
            }
            catch (WebException)
            {
                throw;
            }
        }

        #endregion
    }
}
